Conversation
WalkthroughAdds cache-backed internal boot-device state with TTL and in-flight deduplication, a public cache invalidation method, and tests covering caching, concurrency, and invalidation. Onboarding invokes cache invalidation after boot configuration. UI/store and tests adjust TPM GUID/transfer visibility logic and test setup consolidation. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Service as InternalBootStateService
participant Cache as Cache Manager
participant Disks as DisksService
Client->>Service: getHasInternalBootDevices()
Service->>Cache: get("internal-boot-device-exists")
Cache-->>Service: miss
par concurrent callers
Client->>Service: getHasInternalBootDevices()
Service->>Service: sees in-flight promise -> await
and single loader
Service->>Disks: getDisks()/getArrayData()
Disks-->>Service: disk list / boot info
Service->>Service: compute boolean result
Service->>Cache: set(key, result, TTL)
Cache-->>Service: stored
Service->>Service: resolve in-flight promise
end
Service-->>Client: result (cached / resolved)
Client->>Service: invalidateCachedInternalBootDeviceState()
Service->>Cache: del(key)
Cache-->>Service: deleted
Service->>Service: clear in-flight marker
Service-->>Client: done
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
ba5a6bd to
ba27610
Compare
💡 Codex Reviewboot.type
This check can never report ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.ts`:
- Around line 39-41: The invalidateCachedInternalBootDeviceState() currently
clears the cache key and nulls pendingHasInternalBootDevicesLookup but doesn't
prevent an in-flight loadHasInternalBootDevices() from later repopulating the
cache with stale data; fix by adding a generation token (e.g.,
this.internalBootLookupGeneration: number) that
invalidateCachedInternalBootDeviceState() increments, then have
loadHasInternalBootDevices() capture the current generation into a local
variable and only write to cache and clear pendingHasInternalBootDevicesLookup
if the captured generation still equals this.internalBootLookupGeneration;
update references to pendingHasInternalBootDevicesLookup and
INTERNAL_BOOT_DEVICE_SETUP_CACHE_KEY accordingly and add a regression test that
calls invalidateCachedInternalBootDeviceState() before the first lookup resolves
to ensure the stale result does not repopulate the cache.
In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`:
- Line 410: The call to
this.internalBootStateService.invalidateCachedInternalBootDeviceState() must be
treated as best-effort so its failure cannot flip a successfully completed
mkbootpool flow into an overall failure; wrap that await in its own try/catch,
catch any error and log it as a warning (do not rethrow or return a failure),
then continue returning the successful result from the surrounding handler
(i.e., keep cmdCreatePool, cmdAssignDisk and cmdMakeBootable outcomes as
authoritative). Locate the call to invalidateCachedInternalBootDeviceState in
the onboarding-internal-boot service method and replace the direct await with a
try { await
this.internalBootStateService.invalidateCachedInternalBootDeviceState(); } catch
(err) { /* log warning via existing logger/processLogger */ } so cache
invalidation errors are surfaced as warnings only.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b90fac45-751c-4971-8719-839cf0e0f83d
📒 Files selected for processing (6)
api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.spec.tsapi/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.tsweb/__test__/components/Registration.test.tsweb/src/components/Registration.standalone.vue
api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts
Outdated
Show resolved
Hide resolved
|
This plugin has been deployed to Cloudflare R2 and is available for testing. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1921 +/- ##
==========================================
+ Coverage 51.50% 51.55% +0.04%
==========================================
Files 1024 1024
Lines 70790 70846 +56
Branches 7803 7814 +11
==========================================
+ Hits 36459 36522 +63
+ Misses 34209 34202 -7
Partials 122 122 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts`:
- Around line 227-251: The Logger.prototype.warn spy created as warnSpy in the
test leaks into other tests because it's not restored; update the test (the spec
containing the it('returns success when cache invalidation fails after setup and
logs a warning' ...) block) to restore the spy after assertions by calling
warnSpy.mockRestore() (or ensure restoration in a finally/afterEach cleanup) so
the Logger.prototype.warn implementation is returned to its original behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 0b9d9b31-0128-4735-b6be-201ca3bb1f64
📒 Files selected for processing (7)
api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.spec.tsapi/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.tsapi/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.tsweb/__test__/store/server.test.tsweb/src/components/Registration.standalone.vueweb/src/store/server.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts
- api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.spec.ts
| it('returns success when cache invalidation fails after setup and logs a warning', async () => { | ||
| const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined); | ||
| vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>); | ||
| internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue( | ||
| new Error('cache delete failed') | ||
| ); | ||
| const service = createService(); | ||
|
|
||
| const result = await service.createInternalBootPool({ | ||
| poolName: 'cache', | ||
| devices: ['disk-1'], | ||
| bootSizeMiB: 16384, | ||
| updateBios: false, | ||
| }); | ||
|
|
||
| expect(result.ok).toBe(true); | ||
| expect(result.code).toBe(0); | ||
| expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4); | ||
| expect(internalBootStateService.invalidateCachedInternalBootDeviceState).toHaveBeenCalledTimes( | ||
| 1 | ||
| ); | ||
| expect(warnSpy).toHaveBeenCalledWith( | ||
| 'Failed to invalidate cached internal boot device state after successful setup: cache delete failed' | ||
| ); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Vitest v3, does vi.clearAllMocks() restore vi.spyOn() implementations on class prototypes, or should vi.restoreAllMocks() / spy.mockRestore() be used?
💡 Result:
In Vitest v3, vi.clearAllMocks() does not restore vi.spyOn() implementations (including spies placed on class prototypes)—it only clears call/instance history and keeps the mocked implementation in place. [1]
To put a spied prototype method back to its original implementation/descriptor, use vi.restoreAllMocks() (restores all spies) or spy.mockRestore() (restores one spy). [1][2]
Sources: [1] [2]
🏁 Script executed:
fd -t f "onboarding-internal-boot.service.spec.ts" -x cat -n {}Repository: unraid/api
Length of output: 14868
Restore the Logger.prototype.warn spy after this test.
Line 228 creates a prototype spy but never restores it. vi.clearAllMocks() only clears call history—it does not restore spy implementations—so the mocked behavior leaks into subsequent tests.
🔧 Proposed fix
it('returns success when cache invalidation fails after setup and logs a warning', async () => {
const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
- vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>);
- internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue(
- new Error('cache delete failed')
- );
- const service = createService();
-
- const result = await service.createInternalBootPool({
- poolName: 'cache',
- devices: ['disk-1'],
- bootSizeMiB: 16384,
- updateBios: false,
- });
-
- expect(result.ok).toBe(true);
- expect(result.code).toBe(0);
- expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4);
- expect(internalBootStateService.invalidateCachedInternalBootDeviceState).toHaveBeenCalledTimes(
- 1
- );
- expect(warnSpy).toHaveBeenCalledWith(
- 'Failed to invalidate cached internal boot device state after successful setup: cache delete failed'
- );
+ try {
+ vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>);
+ internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue(
+ new Error('cache delete failed')
+ );
+ const service = createService();
+
+ const result = await service.createInternalBootPool({
+ poolName: 'cache',
+ devices: ['disk-1'],
+ bootSizeMiB: 16384,
+ updateBios: false,
+ });
+
+ expect(result.ok).toBe(true);
+ expect(result.code).toBe(0);
+ expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4);
+ expect(
+ internalBootStateService.invalidateCachedInternalBootDeviceState
+ ).toHaveBeenCalledTimes(1);
+ expect(warnSpy).toHaveBeenCalledWith(
+ 'Failed to invalidate cached internal boot device state after successful setup: cache delete failed'
+ );
+ } finally {
+ warnSpy.mockRestore();
+ }
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('returns success when cache invalidation fails after setup and logs a warning', async () => { | |
| const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined); | |
| vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>); | |
| internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue( | |
| new Error('cache delete failed') | |
| ); | |
| const service = createService(); | |
| const result = await service.createInternalBootPool({ | |
| poolName: 'cache', | |
| devices: ['disk-1'], | |
| bootSizeMiB: 16384, | |
| updateBios: false, | |
| }); | |
| expect(result.ok).toBe(true); | |
| expect(result.code).toBe(0); | |
| expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4); | |
| expect(internalBootStateService.invalidateCachedInternalBootDeviceState).toHaveBeenCalledTimes( | |
| 1 | |
| ); | |
| expect(warnSpy).toHaveBeenCalledWith( | |
| 'Failed to invalidate cached internal boot device state after successful setup: cache delete failed' | |
| ); | |
| }); | |
| it('returns success when cache invalidation fails after setup and logs a warning', async () => { | |
| const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined); | |
| try { | |
| vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>); | |
| internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue( | |
| new Error('cache delete failed') | |
| ); | |
| const service = createService(); | |
| const result = await service.createInternalBootPool({ | |
| poolName: 'cache', | |
| devices: ['disk-1'], | |
| bootSizeMiB: 16384, | |
| updateBios: false, | |
| }); | |
| expect(result.ok).toBe(true); | |
| expect(result.code).toBe(0); | |
| expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4); | |
| expect( | |
| internalBootStateService.invalidateCachedInternalBootDeviceState | |
| ).toHaveBeenCalledTimes(1); | |
| expect(warnSpy).toHaveBeenCalledWith( | |
| 'Failed to invalidate cached internal boot device state after successful setup: cache delete failed' | |
| ); | |
| } finally { | |
| warnSpy.mockRestore(); | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts`
around lines 227 - 251, The Logger.prototype.warn spy created as warnSpy in the
test leaks into other tests because it's not restored; update the test (the spec
containing the it('returns success when cache invalidation fails after setup and
logs a warning' ...) block) to restore the spy after assertions by calling
warnSpy.mockRestore() (or ensure restoration in a finally/afterEach cleanup) so
the Logger.prototype.warn implementation is returned to its original behavior.
🤖 I have created a release *beep* *boop* --- ## [4.30.0](v4.29.2...v4.30.0) (2026-03-18) ### Features * add internal boot step to onboarding flow ([#1881](#1881)) ([337aecc](337aecc)) * Add TPM licensing availability to registration ([#1908](#1908)) ([aa162eb](aa162eb)) * add UPS power ([#1874](#1874)) ([b531aed](b531aed)) * **api:** alert when usb boot has internal boot target ([#1898](#1898)) ([b94df47](b94df47)) * **api:** expose internal boot devices in array GraphQL ([#1894](#1894)) ([0736709](0736709)) * docker overview ([#1855](#1855)) ([9ef1cf1](9ef1cf1)) * **docker:** add update actions to container context menu ([#1867](#1867)) ([4ca3e06](4ca3e06)) * **docker:** disable containers page file modification ([#1870](#1870)) ([aaa0372](aaa0372)) * issues/1597: Temperature Monitoring - Thanks @MitchellThompkins ([a1be458](a1be458)) * New Crowdin updates ([#1809](#1809)) ([a7b3f07](a7b3f07)) * New Crowdin updates ([#1883](#1883)) ([14a8fa8](14a8fa8)) * **onboarding:** add new onboarding flows for Unraid OS ([#1746](#1746)) ([15bd747](15bd747)) * registration and trial actions use Account app ([#1928](#1928)) ([c2c0425](c2c0425)) * share internal boot state ([#1921](#1921)) ([8e4d44d](8e4d44d)) * **web:** show TPM move control for trial licenses ([#1911](#1911)) ([d00fb63](d00fb63)) ### Bug Fixes * Add dedicated TPM license move option ([#1909](#1909)) ([36c56f7](36c56f7)) * allow free USB targets in onboarding internal boot setup ([#1903](#1903)) ([298da54](298da54)) * API key key display truncation ([#1890](#1890)) ([b12f75c](b12f75c)) * **api:** harden PHP wrapper args for newer PHP versions ([#1901](#1901)) ([849f177](849f177)) * **api:** prevent flash notification startup fd exhaustion ([#1893](#1893)) ([4b231ad](4b231ad)) * clear stale onboarding modal session state ([#1904](#1904)) ([23f7836](23f7836)) * consistently clear onboarding draft ([#1916](#1916)) ([199d803](199d803)) * correct graphql-api.log timestamp formatting ([#1918](#1918)) ([243c5a8](243c5a8)) * **deps:** pin dependencies ([#1878](#1878)) ([db88eb8](db88eb8)) * **docker:** change "visit" to "webui" & use correct link ([#1863](#1863)) ([cab0880](cab0880)) * **docker:** improve start/stop UX with visual feedback ([#1865](#1865)) ([c084e25](c084e25)) * **docker:** remove aggressive caching to ensure data correctness ([#1864](#1864)) ([1c1bae8](1c1bae8)) * **docker:** sync template mappings in organizer to prevent false orphan warnings ([#1866](#1866)) ([38a6f0c](38a6f0c)) * onboarding internal-boot warning panel contrast and semantics ([#1927](#1927)) ([bb6f241](bb6f241)) * **onboarding:** add explicit EFI loader path for flash entry ([#1926](#1926)) ([429b438](429b438)) * **onboarding:** extend onboarding refresh timeout ([#1925](#1925)) ([e2a5f44](e2a5f44)) * **onboarding:** persist installed plugins in summary ([#1915](#1915)) ([07f4ebd](07f4ebd)) * **onboarding:** refine storage boot setup UX ([#1900](#1900)) ([1108d0a](1108d0a)) * polish onboarding flow ([#1902](#1902)) ([8742cac](8742cac)) * preserve registration device limits after refresh ([#1905](#1905)) ([234bfc7](234bfc7)) * prevent onboarding on API errors ([#1917](#1917)) ([540d6f9](540d6f9)) * remap TPM guid prefix to 01 ([#1924](#1924)) ([5360b5b](5360b5b)) * Return null for corrupted/invalid API key files and add Connect fixtures test ([#1886](#1886)) ([013e6c5](013e6c5)) * share internal boot state across onboarding ([#1920](#1920)) ([f9b293f](f9b293f)) * too many file descriptors with thousands of notifications ([#1887](#1887)) ([7956987](7956987)) * Treat onboarding patch updates as completed ([#1884](#1884)) ([d03b25e](d03b25e)) * unify onboarding internal boot state refresh ([#1923](#1923)) ([d3032c1](d3032c1)) * **web:** refresh internal boot onboarding state ([#1913](#1913)) ([1ca2129](1ca2129)) * **web:** stop showing callback errors after successful key installs ([#1892](#1892)) ([45f1402](45f1402)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores